package com.sprouting.ops.controller;

import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.sprouting.ops.interfaces.ConstantBase;
import com.sprouting.ops.interfaces.ConstantPath;
import com.sprouting.ops.model.DromaraFileStorage;
import com.sprouting.ops.model.ResultJson;
import com.sprouting.ops.model.qo.DromaraFileStorageQo;
import com.sprouting.ops.service.DromaraFileStorageService;
import com.sprouting.ops.utils.CheckTool;
import com.sprouting.ops.utils.EntityTool;
import lombok.extern.slf4j.Slf4j;
import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.FileStorageService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.Date;

/**
 * 文件存储
 *
 * @author ：LX（长沙麓谷）
 * @date 创建时间： 2025/3/9 11:38
 */
@Slf4j
@Controller
@RequestMapping("fileStorage")
public class DromaraFileStorageController {

    @Resource
    private FileStorageService fileStorageService;
    @Resource
    private DromaraFileStorageService dromaraFileStorageService;


    /**
     * 临时文件在数据库保存的时间，单位天，比如一个文件2天前上传，这里配置了2，则2天后发现状态还没修改为已使用，则删除
     */
    @Value("${ops.customize.storage.temp-del-time:2}")
    private Integer tempDelTime;






     /*
        =============================== 通用方法 start ================================
        =============================== 通用方法 start ================================
        =============================== 通用方法 start ================================
     */

    /**
     * 分页查询文件列表
     * @param page 分页参数
     * @param query 查询参数
     * @return
     */
    @PostMapping("getPage")
    @ResponseBody
    public ResultJson getFileRecordInfoPage(Page page, DromaraFileStorageQo query){
        if (query == null){
            query = new DromaraFileStorageQo();
        }
        return ResultJson.success(dromaraFileStorageService.getFileRecordInfoPage(page, query));
    }


    /**
     * 根据id删除文件
     * @param id 文件id
     * @return
     */
    @PostMapping("/delById")
    @ResponseBody
    public ResultJson delById(String id){
        if (CheckTool.checkNull(id)){
            return ResultJson.errorCheck();
        }

        DromaraFileStorage dromaraFileStorage = dromaraFileStorageService.getById(id);
        if (dromaraFileStorage == null){
            return ResultJson.errorCheck();
        }
        FileInfo fileInfo = dromaraFileStorageService.toFileInfo(dromaraFileStorage);
        fileStorageService.delete(fileInfo);
        return ResultJson.success();
    }


     /*
        =============================== 下载通用方法 start ================================
        =============================== 下载通用方法 start ================================
        =============================== 下载通用方法 start ================================
     */

    /**
     * 根据文件ID，生成一个预签名访问地址，这个地址可访问文件服务器
     * @param id 文件id
     * @return 返回为空的时候，说明不支持预签名
     */
    @GetMapping("/getPreSignature")
    public ResultJson getPreSignature(String id){
        if (CheckTool.checkNull(id)){
            return ResultJson.errorCheck();
        }

        DromaraFileStorage dromaraFileStorage = dromaraFileStorageService.getById(id);
        if (dromaraFileStorage == null){
            log.error("文件获取失败，文件无效");
            return ResultJson.errorCheck();
        }

        FileInfo fileInfo = dromaraFileStorageService.toFileInfo(dromaraFileStorage);

        String url = fileStorageService.generatePresignedUrl(fileInfo, DateUtil.offsetDay(new Date(), 2));
        // 返回为空的时候，说明不支持预签名
        return ResultJson.success(url);
    }

    /**
     * 根据id 下载文件为输出流
     * @param id 文件Id
     * @return
     */
    @RequestMapping("/downOutStreamById")
    @SaIgnore // 生产反而不需要开，开发环境因为认证信息没带过来，只能开启这个，否则md授权不显示
    public void downOutStreamById(String id, HttpServletResponse response) throws IOException {
        if (CheckTool.checkNull(id)){
            return ;
        }

        DromaraFileStorage dromaraFileStorage = dromaraFileStorageService.getById(id);
        if (dromaraFileStorage == null){
            log.error("文件获取失败，文件无效");
            return ;
        }

        FileInfo fileInfo = dromaraFileStorageService.toFileInfo(dromaraFileStorage);

        response.setContentType(fileInfo.getContentType());
        response.setCharacterEncoding("utf-8");
        // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
        String encodeFileName = fileInfo.getOriginalFilename();
        if (StrUtil.isEmpty(encodeFileName)){
            encodeFileName = fileInfo.getFilename();
            if (StrUtil.isEmpty(encodeFileName)){
                encodeFileName = fileInfo.getId();
            }
        }
        String fileName = URLEncoder.encode(encodeFileName, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", String.format("attachment;filename*=utf-8''%s", fileName));
        response.setHeader("Content-Length", String.valueOf(fileInfo.getSize()));

        // 然后可以根据情况写出流
        fileStorageService.download(fileInfo).outputStream(response.getOutputStream());
    }


     /*
        =============================== 文件常用方法 start ================================
        =============================== 文件常用方法 start ================================
        =============================== 文件常用方法 start ================================
     */

    /**
     * 上传文件到后台
     * @param file      要上传的文件
     * @param typePath 图片保存路径，这个路径指的是，比如图片下面的 头像、应用图片、等等，示例：head, app, 还可以 ico/app。前端应该查询后台指定的字典获取
     * @param objectType {@link com.sprouting.ops.interfaces.ConstantBase.FileSourceType} 用于区分从哪个表来的数据
     * @return
     */
    @PostMapping("/uploadFile")
    @ResponseBody
    public ResultJson uploadFile(@RequestParam(value = "file", required = false) MultipartFile file, String typePath, String objectType) {
        // 参数1，用户名，参数2，大类型，比如图片，参数3，小类型，比如用户头像 如果还要细分就是日期了，没必要
        // 参数不需要管 temp之类的，框架会自动拼接
        String path = String.format(ConstantPath.Path.FILE_PATH, StpUtil.getLoginIdAsString(), typePath);

        // 额外添加的数据
        Dict dict = Dict.of(
                EntityTool.getFieldName(DromaraFileStorage::getUserId), StpUtil.getLoginIdAsString(),
                EntityTool.getFieldName(DromaraFileStorage::getDelTime), DateUtil.offsetDay(new Date(), tempDelTime),
                EntityTool.getFieldName(DromaraFileStorage::getTempFile), ConstantBase.TempFile.TEMP
        );

        FileInfo upload = fileStorageService.of(file)
                // 在配置文件中已经设置了路径，但那只是基础路径，实际业务中可能还有下级路径。目前存储基础路径为：E:/temps/ 。设置的下级路径为 asset/，也就是E:/temps/asset/
                // 这里还要在下级进行分
                .setPath(path)
                // 关联对象id，也就是这个可能和那个表的关联,比如是用户的头像，这里可能就是用户id了.这里也可能外面传入（这个需要转为正式的时候添加） OpsUploadFile.bindId
                .setObjectId(null)
                // 关联对象类型，我们这里一般是 OpsUploadFile.bindType，也就是 ConstantBase.FileSourceType 数据 （这个需要转为正式的时候添加）
                .setObjectType(objectType)
                .setAttr(dict)
                // 计算 MD5
                .setHashCalculatorMd5()
                .upload();

        return ResultJson.success(upload);
    }

     /*
        =============================== 文件常用方法 end ================================
        =============================== 文件常用方法 end ================================
        =============================== 文件常用方法 end ================================
     */

    /*
        =============================== 图片常用方法 start ================================
        =============================== 图片常用方法 start ================================
        =============================== 图片常用方法 start ================================
     */

    /**
     * 上传图片到后台
     * @param file      要上传的文件
     * @param imageTypePath 图片保存路径，这个路径指的是，比如图片下面的 头像、应用图片、等等，示例：head, app, 还可以 ico/app。前端应该查询后台指定的字典获取
     * @param objectType {@link com.sprouting.ops.interfaces.ConstantBase.FileSourceType} 用于区分从哪个表来的数据
     * @return
     */
    @PostMapping("/uploadImage")
    @ResponseBody
    public ResultJson uploadImage(@RequestParam(value = "file", required = false) MultipartFile file, String imageTypePath, String objectType) {
        String msg = checkImages(file, imageTypePath);
        if (msg != null) {
            return ResultJson.error(msg);
        }

        // 参数1，用户名，参数2，大类型，比如图片，参数3，小类型，比如用户头像 如果还要细分就是日期了，没必要
        // 参数不需要管 temp之类的，框架会自动拼接
        String path = String.format(ConstantPath.Path.IMAGES_PATH, StpUtil.getLoginIdAsString(), imageTypePath);

        // 额外添加的数据
        Dict dict = Dict.of(
                EntityTool.getFieldName(DromaraFileStorage::getUserId), StpUtil.getLoginIdAsString(),
                EntityTool.getFieldName(DromaraFileStorage::getDelTime), DateUtil.offsetDay(new Date(), tempDelTime),
                EntityTool.getFieldName(DromaraFileStorage::getTempFile), ConstantBase.TempFile.TEMP
	            );

        FileInfo upload = fileStorageService.of(file)
                // 在配置文件中已经设置了路径，但那只是基础路径，实际业务中可能还有下级路径。目前存储基础路径为：E:/temps/ 。设置的下级路径为 asset/，也就是E:/temps/asset/
                // 这里还要在下级进行分
                .setPath(path)
                // 关联对象id，也就是这个可能和那个表的关联,比如是用户的头像，这里可能就是用户id了.这里也可能外面传入（这个需要转为正式的时候添加） OpsUploadFile.bindId
                .setObjectId(null)
                // 关联对象类型，我们这里一般是 OpsUploadFile.bindType，也就是 ConstantBase.FileSourceType 数据 （这个需要转为正式的时候添加）
                .setObjectType(objectType)
                .setAttr(dict)
                // 计算 MD5
                .setHashCalculatorMd5()
                .upload();

        return ResultJson.success(upload);
    }

    /**
     * 根据url删除图片
     * @param url 上传文件路径 FileInfo.url
     * @return
     */
    @PostMapping("delImageByUrl")
    @ResponseBody
    public ResultJson delImageByUrl(String url){
        if (CheckTool.checkNull(url)){
            return ResultJson.errorCheck();
        }

        FileInfo fileInfo = dromaraFileStorageService.getByUrl(url);
        if (fileInfo == null){
            return ResultJson.success();
        }

        // asset/images/1831381431835279360/site/67d188de155ddf1faa202014.png
        fileStorageService.delete(fileInfo);
        return ResultJson.success();
    }

    /**
     * 根据id 下载文件为字节，然后转换为base64
     * @param id 文件Id
     * @return
     */
    @PostMapping("/downByteByIdBase64")
    @ResponseBody
    public ResultJson downByteByIdBase64(String id){
        if (CheckTool.checkNull(id)){
            return ResultJson.errorCheck();
        }

        DromaraFileStorage dromaraFileStorage = dromaraFileStorageService.getById(id);
        if (dromaraFileStorage == null){
            log.error("文件获取失败，文件无效");
            return ResultJson.errorCheck();
        }

        FileInfo fileInfo = dromaraFileStorageService.toFileInfo(dromaraFileStorage);

        byte[] bytes = fileStorageService.download(fileInfo).bytes();
        BufferedImage bufferedImage = ImgUtil.toImage(bytes);
        return ResultJson.success(ImgUtil.toBase64(bufferedImage, "png"));
    }



    /*
        =============================== 不常用方法 start ================================
        =============================== 不常用方法 start ================================
        =============================== 不常用方法 start ================================
     */

    /**
     * 根据url查询文件是否还存在
     * @param url 文件链接
     * @return true 文件还存在，false 文件不存在
     */
    @PostMapping("/existsByUrl")
    @ResponseBody
    public ResultJson existsByUrl(String url){
        if (CheckTool.checkNull(url)){
            return ResultJson.errorCheck();
        }

        FileInfo fileInfo = dromaraFileStorageService.getByUrl(url);
        if (fileInfo == null){
            return ResultJson.errorCheck();
        }
        // true 文件还存在，false 文件不存在
        boolean exists = fileStorageService.exists(fileInfo);
        return ResultJson.success(exists);
    }

    /**
     * 根据文件id查询文件是否存在
     * 文件记录存在，但文件不存在，一样会返回false，这里判断的是物理文件是否存在，而不是只判断数据库记录
     * @param id 文件id
     * @return true 文件还存在，false 文件不存在
     */
    @PostMapping("/existsById")
    @ResponseBody
    public ResultJson existsById(String id){
        if (CheckTool.checkNull(id)){
            return ResultJson.errorCheck();
        }

        DromaraFileStorage dromaraFileStorage = dromaraFileStorageService.getById(id);
        if (dromaraFileStorage == null){
            return ResultJson.errorCheck();
        }

        FileInfo fileInfo = dromaraFileStorageService.toFileInfo(dromaraFileStorage);
        // true 文件还存在，false 文件不存在
        boolean exists = fileStorageService.exists(fileInfo);
        return ResultJson.success(exists);
    }

    /*
        =============================== 私有方法 start ================================
        =============================== 私有方法 start ================================
        =============================== 私有方法 start ================================
     */

    /**
     * 检查图片是否合法
     * @param file 图片文件
     * @param imagePath 图片保存路径，这个路径指的是，比如图片下面的 头像、应用图片、等等，示例：head, app, 还可以 ico/app
     * @return 非null异常
     */
    private String checkImages(MultipartFile file, String imagePath){
        if (file == null || file.isEmpty()) {
            return "请上传图片文件";
        }
        if (CheckTool.checkNull(imagePath)){
            return "必须指定图片类型";
        }
        String originalFilename = file.getOriginalFilename();
        if (CheckTool.checkNull(originalFilename)) {
            return "获取文件名失败";
        }
        String suffix = FileUtil.getSuffix(originalFilename).toLowerCase();
        if (CheckTool.checkNull(suffix)) {
            // 检查特殊情况下的文件名不符合
            return "非法上传图片名，不支持的后缀";
        }
        if ("png".equals(suffix) || "jpg".equals(suffix) || "jpeg".equals(suffix) || "ico".equals(suffix)){

        } else {
            return "图片只支持png、jpg、jpeg、ico格式";
        }

        return null;
    }


}
